// package handler：HTTP请求处理器包，负责实现博客文档的MD文件上传与解析入库逻辑
// 该文件专注于处理多文件（MD格式）上传，包含临时文件管理、格式校验、内容解析及数据库存储
package handler

import (
	"fmt"
	"github.com/gin-gonic/gin" // Gin Web框架：处理HTTP请求、表单文件接收、响应返回
	"go-blog/model"
	"go-blog/util"

	"gorm.io/gorm"  // GORM：数据库ORM框架，用于将解析后的MD内容存入数据库
	"net/http"      // 标准库HTTP包：提供HTTP状态码常量
	"os"            // 标准库OS包：处理文件系统操作（创建目录、删除文件）
	"path/filepath" // 标准库路径处理包：拼接文件路径、获取文件后缀
	"strings"       // 标准库字符串处理包：转换字符串大小写（校验文件后缀）
)

// tempUploadDir：临时上传目录常量，用于暂存用户上传的MD文件
// 暂存目的：解析MD内容前需读取文件，解析完成后会删除临时文件，避免磁盘空间占用
const tempUploadDir = "./temp_upload"

// init函数：包初始化时自动执行，用于创建临时上传目录
// 作用：确保上传功能依赖的目录存在，避免因目录缺失导致上传失败
// 权限0755：所有者（rwx）可读写执行，组用户和其他用户（r-x）仅可读可执行，符合安全规范
func init() {
	// os.MkdirAll：递归创建目录（若上级目录不存在也会创建），避免手动创建目录的麻烦
	if err := os.MkdirAll(tempUploadDir, 0755); err != nil {
		// 目录创建失败时panic：临时目录是上传功能的核心依赖，创建失败则程序无法正常提供上传服务
		panic("创建临时上传目录失败：" + err.Error())
	}
}

// UploadDoc：MD文件/文件夹上传接口处理器
// @Summary 上传MD文件（支持多文件）
// @Security BearerAuth
// @Description 接收用户上传的MD格式文件，解析内容后存入数据库，支持批量上传
// @Tags 文档
// @Accept multipart/form-data
// @Produce json
// @Param category formData string false "文档分类（tech/life/other，默认other）"
// @Param files formData file true "MD文件列表（支持多个，仅接受.md格式）"
// @Success 200 {object} map[string]interface{} "{"code":0,"msg":"上传完成","data":{"success_count":1,"fail_files":["test.txt（非MD文件）"]}}"
// @Failure 400 {object} map[string]interface{} "{"code":400,"msg":"文件上传失败（如未选择文件、格式错误）"}"
// @Failure 401 {object} map[string]interface{} "{"code":401,"msg":"未登录（需携带有效Token）"}"
// @Failure 500 {object} map[string]interface{} "{"code":500,"msg":"服务器错误（如数据库异常）"}"
// @Security ApiKeyAuth
// @Router /docs/upload [post]
// 接口定义：POST /api/docs/upload，支持多文件上传（单个MD文件或多个MD文件组成的文件夹）
// 核心逻辑：接收文件→校验格式→暂存文件→解析内容→存入数据库→清理临时文件→返回结果
// 前置条件：需登录（通过中间件如JWT验证，在上下文c中注入"user_id"）
func UploadDoc(db *gorm.DB) gin.HandlerFunc {
	return func(c *gin.Context) {
		// 1. 获取当前登录用户ID：作为文档的作者ID（关联用户表）
		// "user_id"由登录中间件（如JWT校验中间件）在请求处理前注入上下文，确保用户已登录
		// 类型断言为uint：与model.Doc的AuthorID字段类型一致（数据库中AuthorID为无符号整数）
		userID, _ := c.Get("user_id")
		authorID := userID.(uint)

		// 2. 获取文档分类参数：从表单中获取category，无合法分类时使用默认值
		// c.PostForm("category")：读取表单中name为"category"的字段（前端需通过表单提交分类）
		category := c.PostForm("category")
		// 校验分类合法性：调用model层的IsValidCategory方法（如判断是否为tech/life/other）
		// 若分类不合法（如空值或非法字符串），默认设为model.CategoryOther（其他分类），保证数据合法性
		if !model.IsValidCategory(category) {
			category = model.CategoryOther
		}

		// 3. 接收上传的文件：处理multipart/form-data类型的表单（文件上传专用表单格式）
		// c.MultipartForm()：解析表单中的文件和普通字段，返回表单数据结构体
		form, err := c.MultipartForm()
		if err != nil {
			// 表单解析失败（如请求格式错误、文件过大），返回400 Bad Request
			c.JSON(http.StatusBadRequest, gin.H{
				"code": 400,
				"msg":  "文件上传失败：" + err.Error(), // 携带具体错误信息，便于前端排查问题
			})
			return
		}

		// 从表单中获取name为"files"的文件列表：前端需将上传的文件绑定到该name字段（支持多文件）
		files := form.File["files"]
		// 校验是否选择文件：若文件列表为空，返回错误提示
		if len(files) == 0 {
			c.JSON(http.StatusBadRequest, gin.H{
				"code": 400,
				"msg":  "请选择MD文件",
			})
			return
		}

		// 4. 批量处理每个上传的文件：统计成功/失败数量，记录失败文件信息
		successCount := 0              // 成功上传并入库的文件数量
		failFiles := make([]string, 0) // 存储上传失败的文件名称及原因（用于前端展示）

		// 循环处理每个文件：逐个校验、解析、入库，避免单个文件失败影响整体流程
		for _, file := range files {
			// 4.1 校验文件格式：仅允许MD文件（后缀为.md）
			// filepath.Ext(file.Filename)：获取文件后缀（如"test.md"返回".md"）
			// strings.ToLower()：将后缀转为小写，避免大小写问题（如".MD"也视为合法格式）
			fileExt := strings.ToLower(filepath.Ext(file.Filename))
			if fileExt != ".md" {
				// 非MD文件：记录失败信息，跳过后续处理
				failFiles = append(failFiles, fmt.Sprintf("%s（非MD文件）", file.Filename))
				continue
			}

			// 4.2 保存文件到临时目录：为后续解析MD内容做准备
			// filepath.Join：安全拼接临时文件路径（避免路径注入问题），格式为"临时目录/文件名"
			tempFilePath := filepath.Join(tempUploadDir, file.Filename)
			// c.SaveUploadedFile：Gin提供的工具函数，将上传的文件保存到指定路径
			if err := c.SaveUploadedFile(file, tempFilePath); err != nil {
				// 保存失败（如磁盘空间不足、权限不足）：记录失败信息，跳过后续处理
				failFiles = append(failFiles, fmt.Sprintf("%s（保存失败：%v）", file.Filename, err))
				continue
			}

			// 4.3 解析MD文件内容：调用自定义工具函数提取标题和正文
			// util.ParseMDContent：需在util包中实现，逻辑示例：
			// - 读取文件内容，以首行"# 标题"作为文档标题，其余内容作为正文
			// - 或通过特定格式（如YAML Front Matter）提取元数据和内容
			title, content, parseErr := util.ParseMDContent(tempFilePath)
			if parseErr != nil {
				// 解析失败（如文件损坏、格式不规范）：删除临时文件（避免残留），记录失败信息
				os.Remove(tempFilePath)
				failFiles = append(failFiles, fmt.Sprintf("%s（解析失败：%v）", file.Filename, parseErr))
				continue
			}

			// 4.4 将解析后的内容存入数据库：构建Doc模型并执行创建操作
			doc := model.Doc{
				Title:    title,    // 从MD文件解析的标题
				Content:  content,  // 从MD文件解析的正文
				Category: category, // 从表单获取的分类（或默认分类）
				AuthorID: authorID, // 当前登录用户ID（文档作者）
				// CreatedAt/UpdatedAt：由GORM自动填充（需在model.Doc中配置autoCreateTime/autoUpdateTime标签）
			}
			// 执行数据库创建操作：将文档数据插入docs表
			if err := db.Create(&doc).Error; err != nil {
				// 入库失败（如数据库连接异常、字段验证失败）：删除临时文件，记录失败信息
				os.Remove(tempFilePath)
				failFiles = append(failFiles, fmt.Sprintf("%s（数据库失败：%v）", file.Filename, err))
				continue
			}

			// 4.5 处理成功：更新成功计数，删除临时文件（解析和入库已完成，无需保留）
			successCount++
			os.Remove(tempFilePath) // 清理临时文件，释放磁盘空间
		}

		// 5. 返回上传结果：告知前端整体上传情况及详细失败信息
		c.JSON(http.StatusOK, gin.H{
			"code": 0, // 业务成功码：0表示上传流程正常完成（即使部分文件失败）
			// 汇总信息：清晰展示成功和失败数量，提升用户体验
			"msg": fmt.Sprintf("上传完成：成功%d个，失败%d个", successCount, len(failFiles)),
			"data": gin.H{
				"success_count": successCount, // 成功数量
				"fail_files":    failFiles,    // 失败文件列表（含原因），便于前端展示错误详情
			},
		})
	}
}
